﻿namespace Chameleon.Data
{
	using Microsoft.VisualStudio.TestTools.UnitTesting;
	using System;
	using System.Collections.Generic;
	using System.Linq;

	/// <summary>
	/// Fixture for testing the MimicDepository type.
	/// </summary>
	
	[TestClass]
	public class MimicDepositoryTesting
	{
		#region Basic Types

			/// <summary>
			/// Basic type for testing.
			/// </summary>
			
			class TypeA {}


			/// <summary>
			/// Basic type for testing.
			/// </summary>
			
			class TypeB {}

		#endregion


		#region Query

			/// <summary>
			/// When the Query method is called, it should always return a queryable interface of a specific type.
			/// </summary>
		
			[TestMethod]
			public void Query_ShouldReturnQueryable()
			{
				using ( var db = new MimicDepository() )
				{
					var a = db.Query<TypeA>();
					Assert.IsInstanceOfType( a, typeof(IQueryable<TypeA>) );
				}
			}


			/// <summary>
			/// When no items are present in the depository, the Query method should return an empty set.
			/// </summary>

			[TestMethod]
			public void Query_WhenEmpty_ShouldReturnEmptyCollection()
			{
				using ( var db = new MimicDepository() )
				{
					var a = db.Query<TypeA>();
					Assert.IsFalse( a.Any() );
				}
			}

		#endregion


		#region Insert

			/// <summary>
			/// When an item that does not already exist is inserted into the depository, but not committed, a reference to that item 
			/// should be returned but it should not be present via the queryable interface.
			/// </summary>
			/// <remarks>
			/// This is intended to mimic the functionality of a standard data context - items awaiting insert will not show up in the
			/// database until they are committed.
			/// </remarks>
			
			[TestMethod]
			public void Insert_WhenNotExisting_BeforeCommit_ShouldNotExistInQuery()
			{
				var e = new TypeA();
				
				using ( var db = new MimicDepository() )
				{
					var a = db.Insert( e );

					// reference should match, but item should not be in the query
					Assert.AreEqual( a, e );
					Assert.IsFalse( db.Query<TypeA>().Contains( e ) );
				}
			}


			/// <summary>
			/// When an item is inserted twice into the depository, an exception should be thrown.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Insert_WhenAlreadyInserted_ShouldThrowException()
			{
				var e = new TypeA();

				using ( var db = new MimicDepository() )
				{
					db.Insert( e );
					db.Insert( e );
				}
			}


			/// <summary>
			/// When an attempt is made to insert an item that already exists in the depository, an exception should be thrown.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Insert_WhenExisting_ShouldThrowException()
			{
				var e = new TypeA();

				using ( var db = new MimicDepository( Enumerable.Repeat( e, 1 ) ) )
					db.Insert( e );
			}


			/// <summary>
			/// Following a commit, any inserted items should appear in the query.
			/// </summary>
			
			[TestMethod]
			public void Insert_WhenItemsAwaitingInsert_AfterCommit_ShouldBeInQuery()
			{
				using ( var db = new MimicDepository() )
				{
					var e = new List<object>() { new TypeA(), new TypeA(), new TypeB() };
									
					foreach ( var i in e )
						db.Insert( i );

					// perform commit
					db.Submit();

					// ensure items are now in the query
					Assert.IsTrue( e.OfType<TypeA>().All( x => db.Query<TypeA>().Contains( x ) ) );
					Assert.IsTrue( e.OfType<TypeB>().All( x => db.Query<TypeB>().Contains( x ) ) );

					foreach ( var i in db.Query<TypeA>() )
						Console.WriteLine( "HAH" );
				}
			}

		#endregion


		#region Attach

			/// <summary>
			/// When an non-existing item is attached to the depository, an exception should be thrown.
			/// </summary>
			
			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Attach_WhenNotExisting_ShouldThrow()
			{
				using ( var db = new MimicDepository() )
				{
					var e = new TypeA();
					db.Attach( e );

					Assert.IsTrue( db.Query<TypeA>().Contains( e ) );
				}
			}


			/// <summary>
			/// When an existing item is attached to the depository, we should treat it as though it already exists,
			/// and thus should be visible through the query.
			/// </summary>
			
			[TestMethod]
			public void Attach_WhenExisting_ShouldBeInQuery()
			{
				var e = new List<object>() { new TypeA(), new TypeA(), new TypeB() };

				using ( var db = new MimicDepository( e ) )
				{
					var a = e.OfType<TypeA>().First();
					db.Attach( a );

					Assert.IsTrue( db.Query<TypeA>().Contains( a ) );
				}
			}


			/// <summary>
			/// When an item is attached that is already pending an insert, an exception should be thrown.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Attach_WhenInserted_ShouldThrow()
			{
				using ( var db = new MimicDepository() )
				{
					var e = db.Insert( new TypeA() );
					db.Attach( e );
				}
			}


			/// <summary>
			/// When an item is attached that is already pending a delete, an exception should be thrown.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Attach_WhenDeleted_ShouldThrow()
			{
				var e = new List<object>() { new TypeA(), new TypeA(), new TypeB() };

				using ( var db = new MimicDepository(e) )
				{
					var a = e.First();
					db.Delete( a );
					db.Attach( a );
				}
			}


			/// <summary>
			/// When an item that has been returned as a query result is attached, an exception should be thrown.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Attach_WhenPreviouslyReturnedByQuery_ShouldThrow()
			{
				var e = new List<object>() { new TypeA(), new TypeA(), new TypeB() };

				using ( var db = new MimicDepository( e ) )
				{
					// todo: need a way to intercept when First/Last/ElementAt is called directly on the IQueryable
					var a = db.Query<TypeA>().ToArray().First();
					db.Attach( a );
				}
			}

		#endregion


		#region Delete

			/// <summary>
			/// Prior to commit, an item marked for deletion should still be present in the query.
			/// </summary>
			
			[TestMethod]
			public void Delete_WhenExisting_BeforeCommit_ShouldStillExistInQuery()
			{
				var e = new TypeA();
				var s = new TypeA();
				var existing = new List<object>() { e, s };

				using ( var db = new MimicDepository( existing ) )
				{
					db.Attach( e );
					db.Delete( e );
					Assert.IsTrue( db.Query<TypeA>().Contains( e ) );
					Assert.IsTrue( db.Query<TypeA>().Contains( s ) );
				}
			}


			/// <summary>
			/// Following a commit, an item marked for deletion should not be present in the query.
			/// </summary>

			[TestMethod]
			public void Delete_WhenExisting_AfterCommit_ShouldNotExistInQuery()
			{
				var e = new TypeA();
				var s = new TypeA();
				var existing = new List<object>() { e, s };

				using ( var db = new MimicDepository( existing ) )
				{
					db.Attach( e );
					db.Delete( e );
					db.Submit();
					Assert.IsFalse( db.Query<TypeA>().Contains( e ) ); // e should no longer be present
					Assert.IsTrue( db.Query<TypeA>().Contains( s ) );  // s should still be present
				}
			}


			/// <summary>
			/// When an item does not exist in the depository, attempting to delete it should throw an exception.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Delete_WhenNotExisting_ShouldThrow()
			{
				using ( var db = new MimicDepository() )
				{
					var e = new TypeA(); // don't insert e
					db.Delete( e );
				}
			}


			/// <summary>
			/// When an item is pending insert, but has not been committed, attempting to delete it should throw an exception.
			/// </summary>

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void Delete_WhenPendingInsert_ShouldThrow()
			{
				using ( var db = new MimicDepository() )
				{
					var e = db.Insert( new TypeA() ); // insert e
					db.Delete( e );
				}
			}

		#endregion
	}
}
